-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local log = require 'mc.logging'
local consts = require 'constants'
local skynet = require 'skynet'
local json = require 'cjson'
local protocol = require 'canbus.protocol'
local definition = require 'canbus.definition'
local sender_instance = require 'tcp.sender'
local tcp_mgmt_instance = require 'tcp.tcp_mgmt'

local module = class{}

local DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'

function module:ctor(mdb_obj, name)
    self.mdb_obj = mdb_obj
    self.name = name
    self.sync_mode = mdb_obj.SyncMode
    self.sync_object_name = nil  -- 延迟获取，可能还未上树无法获取
    self.sync_service_name = mdb_obj.SyncObjectName:match('"Service":%s*"([^"]+)"')
    self.sync_object_path = mdb_obj.SyncObjectName:match('"Path":%s*"([^"]+)"')
    self.sync_interval = mdb_obj.SyncInterval
    self.ttl = mdb_obj.TTL
    self.listen_slot = nil  -- 保留监听信号，避免被自动回收
    self.sync_datas = {}  -- 存放每个同步对象所同步过来的数据
    self.intf_to_prop = {}
    self.sync_total_count = 0
    self.sync_failed_count = 0
    self.send_msg_func = nil
    self.enabled = false  -- 发送者且使能的场景才需要同步
    self.sync_task_running = false -- 同步任务执行标记
    self.sync_count = {} -- 统计同步次数
    self.count_task_running = false -- 统计任务执行标记
end

local function get_prop(bus, service, path, intf, prop)
    local ok, result = pcall(function()
        return bus:call(service, path, DBUS_PROPERTIES, 'Get', 'ss', intf, prop):value()
    end)
    if not ok then
        log:info('get prop [%s:%s] failed, %s', intf, prop, result)
        return nil
    end
    return result
end

local function create_sync_items(intf_to_prop, sync_datas, interface, props)
    local prop_table = {}
    for _, prop in pairs(props) do
        prop_table[prop] = true
    end
    intf_to_prop[interface] = prop_table
    sync_datas[interface] = {}
end

local function get_sync_object_name(self, bus)
    log:notice('start get sync object name')
    skynet.fork_once(function()
        while not self.sync_object_name do
            self.sync_object_name = get_prop(bus, self.sync_service_name, self.sync_object_path,
                'bmc.kepler.Object.Properties', 'ObjectName')
            skynet.sleep(300)
        end
        log:info('sync object name [%s]', self.sync_object_name)
    end)
end

function module:start_sync_task(bus)
    if self.sync_mode == consts.SYNC_MODE_REALTIME then
        self:sync_realtime(bus)
    else
        skynet.fork_once(function ()
            self:sync_period(bus)
        end)
    end
    log:notice('[%s] start sync, sync mode [%s]', self.name, self.sync_mode)
end

-- 轮询周期与老化时间的错误配置不影响同步功能，取安全值即可，但需要记录定位日志
local function update_interval_and_ttl(self)
    -- 超过范围时使用边界值
    if self.sync_interval < consts.SYNC_PERIOD_MIN then
        self.sync_interval = consts.SYNC_PERIOD_MIN
    elseif self.sync_interval > consts.SYNC_PERIOD_MAX then
        self.sync_interval = consts.SYNC_PERIOD_MAX
    end

    if self.ttl == 0 then  -- 0为不监控老化
        return
    end

    if self.sync_interval >= self.ttl then
        self.ttl = self.sync_interval * 3  -- 如果老化时间小于检测周期，则取周期时间的3倍
    end
end

function module:initialize(bus, communication_type, tcp_enabled_sig)
    update_interval_and_ttl(self)
    if communication_type == consts.COMMUNICATION_TYPE_CAN then
        self.send_msg_func = function (msg)
            protocol.send_msg(definition.can_cmds.PROPERTY_SYNC_REQ, json.encode(msg))
        end
    else
        self.send_msg_func = function (msg)
            sender_instance.get_instance():send_msg(msg, 'property')
        end
    end
    get_sync_object_name(self, bus)
    -- 初始化需要处理的接口属性列表
    for _, sync_item in pairs(self.mdb_obj.SyncObjectItems) do
        create_sync_items(self.intf_to_prop, self.sync_datas, table.unpack(sync_item))
    end
    tcp_enabled_sig:on(function()
        self:update_sync(bus)
    end)
    if tcp_mgmt_instance:get_instance():get_sync_role() ~= consts.DATASYNC_ROLE_SENDER then
        return
    end
    -- 开启同步任务
    self.enabled = true
    self.sync_task_running = true
    self:start_sync_task(bus)
end

function module:update_sync(bus)
    if tcp_mgmt_instance:get_instance():get_sync_role() == consts.DATASYNC_ROLE_SENDER then
        -- 已开启状态不需要更新
        if self.enabled then
            return
        end
        self.enabled = true
        -- 只有周期性同步可能出现已经停止使能但还在执行的情况
        -- 所以如果正在周期性同步，让原协程继续执行即可
        if self.sync_task_running then
            log:info('period sync task [%s] continue', self.name)
            return
        end
        -- 重启同步任务
        self:start_sync_task(bus)
        return
    end
    -- 非发送者的处理方式
    -- 已关闭状态不需要更新
    if not self.enabled then
        return
    end
    self.enabled = false
    -- 如果是实时同步需要关闭信号
    if self.listen_slot then
        self.listen_slot:free()
        self.listen_slot = nil
        self.sync_task_running = false
        log:info('stop realtime sync [%s] task', self.name)
    end
end

-- 用于统计监听的属性变更，在过于频繁的时候输出日志
-- 格式为{Interface = { Prop = Time }}
function module:start_sync_count()
    -- 防止使能状态是在1min内变更
    if self.count_task_running then
        return
    end
    self.count_task_running = true
    skynet.fork_once(function ()
        while self.enabled do
            skynet.sleep(6000)  -- 每1min统计一次各个属性的同步频率（6000）
            local prop, count
            for intf, prop_count in pairs(self.sync_count) do
                prop, count = next(prop_count)
                if count > 60 then    -- 认为属性最多每秒检测一次，所以1min最多变更60次，超过认为变更频率过快
                    log:warn('[%s][%s] abnormal property change frequency', intf, prop)
                    prop_count[prop] = 0
                end
            end
        end
        self.count_task_running = false
    end)
end

function module:sync_realtime(bus)
    local match_rule = require('sd_bus.org_freedesktop_dbus').MatchRule
    local sig = match_rule.signal('PropertiesChanged', DBUS_PROPERTIES):with_path_namespace(self.sync_object_path)
    -- 开启监听
    local cur_intf, vals
    self:start_sync_count()
    self.listen_slot = bus:match(sig, function(msg)
        cur_intf, vals = msg:read('sa{sv}as')
        if not self.intf_to_prop[cur_intf] then
            return
        end
        if not self.sync_datas[cur_intf] then
            self.sync_datas[cur_intf] = {}
        end
        for prop, val in pairs(vals) do
            if not self.intf_to_prop[cur_intf][prop] then
                goto continue
            end
            if not self.sync_count[cur_intf] then
                self.sync_count[cur_intf] = {[prop] = 1}
            else
                self.sync_count[cur_intf][prop] = self.sync_count[cur_intf][prop] and self.sync_count[cur_intf][prop] + 1 or 1
            end
            self.sync_datas[cur_intf][prop] = val:value()
            self:send_sync_item(cur_intf, prop, self.sync_datas[cur_intf][prop])
            ::continue::
        end
    end)
end

function module:sync_period(bus)
    local interval = self.sync_interval * consts.SKYNET_ONE_SECOND
    local value
    local result = true
    while self.enabled do
        skynet.sleep(interval)
        for interface, props in pairs(self.intf_to_prop) do
            for prop, _ in pairs(props) do
                value = get_prop(bus, self.sync_service_name, self.sync_object_path, interface, prop)
                if value then
                    self.sync_datas[interface][prop] = value
                else
                    result = false
                end
            end
        end
        if not result then
            self.sync_failed_count = self.sync_failed_count + 1
            result = true
        end
        self:send_sync_item()
    end
    self.sync_task_running = false
    log:info('stop period sync [%s] task', self.name)
end

-- 封装信息
function module:send_sync_item(interface, prop, value)
    self.sync_total_count = self.sync_total_count + 1
    --  同步对象名获取失败时不同步
    if not self.sync_object_name then
        return
    end

    local msg
    -- 周期性同步直接返回整个数据
    if self.sync_mode == consts.SYNC_MODE_PERIOD then
        msg = self.sync_datas
    else
        msg = {}
        msg[interface] = {}
        msg[interface][prop] = value
    end
    -- 格式化datas
    local res = {}
    res['SrcName'] = self.name
    res['UpdateName'] = self.sync_object_name
    res['Data'] = msg

    self.send_msg_func(res)
end

return module